{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b518b04cbfe0"
      },
      "source": [
        "##### Copyright 2020 The TensorFlow Authors."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "cellView": "form",
        "id": "906e07f6e562"
      },
      "outputs": [],
      "source": [
        "#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
        "# you may not use this file except in compliance with the License.\n",
        "# You may obtain a copy of the License at\n",
        "#\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\n",
        "#\n",
        "# Unless required by applicable law or agreed to in writing, software\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
        "# See the License for the specific language governing permissions and\n",
        "# limitations under the License."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "a81c428fc2d3"
      },
      "source": [
        "# 전이 학습 및 미세 조정"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "3e5a59f0aefd"
      },
      "source": [
        "<table class=\"tfo-notebook-buttons\" align=\"left\">\n",
        "  <td><a target=\"_blank\" href=\"https://www.tensorflow.org/guide/keras/transfer_learning\"><img src=\"https://www.tensorflow.org/images/tf_logo_32px.png\">TensorFlow.org에서 보기</a></td>\n",
        "  <td><a target=\"_blank\" href=\"https://colab.research.google.com/github/tensorflow/docs-l10n/blob/master/site/ko/guide/keras/transfer_learning.ipynb\"><img src=\"https://www.tensorflow.org/images/colab_logo_32px.png\">Google Colab에서 실행</a></td>\n",
        "  <td><a target=\"_blank\" href=\"https://github.com/tensorflow/docs-l10n/blob/master/site/ko/guide/keras/transfer_learning.ipynb\">     <img src=\"https://www.tensorflow.org/images/GitHub-Mark-32px.png\">    GitHub에서 소스 보기</a></td>\n",
        "  <td><a href=\"https://storage.googleapis.com/tensorflow_docs/docs-l10n/site/ko/guide/keras/transfer_learning.ipynb\"><img src=\"https://www.tensorflow.org/images/download_logo_32px.png\">노트북 다운로드</a></td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "8d4ac441b1fc"
      },
      "source": [
        "## Setup"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9a7e9b92f963"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "import tensorflow as tf\n",
        "from tensorflow import keras"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "00d4c41cfe2f"
      },
      "source": [
        "## 시작하기\n",
        "\n",
        "**전이 학습**은 한 가지 문제에 대해 학습한 기능을 가져와서 비슷한 새로운 문제에 활용하는 것으로 구성됩니다. 예를 들어, 라쿤을 식별하는 방법을 배운 모델의 기능은 너구리를 식별하는 모델을 시작하는 데 유용할 수 있습니다.\n",
        "\n",
        "전송 학습은 일반적으로 풀 스케일 모델을 처음부터 훈련하기에는 데이터세트에 데이터가 너무 적은 작업에 대해 수행됩니다.\n",
        "\n",
        "딥 러닝의 맥락에서 전이 학습의 가장 일반적인 구현은 다음 워크플로와 같습니다.\n",
        "\n",
        "1. 이전에 훈련된 모델에서 레이어를 가져옵니다.\n",
        "2. 추후 훈련 라운드 중에 포함 된 정보가 손상되지 않도록 고정하십시오.\n",
        "3. 고정된 레이어 위에 훈련할 수 있는 새 레이어를 추가합니다. 해당 레이어는 기존 기능을 새로운 데이터세트에 대한 예측으로 전환하는 방법을 배웁니다.\n",
        "4. 데이터세트에서 새로운 레이어를 훈련합니다.\n",
        "\n",
        "마지막으로 선택적인 단계는 **fine-tuning**입니다. 이 단계는 위에서 얻은 전체 모델(또는 모델의 일부)을 동결 해제하고 학습 속도가 매우 낮은 새로운 데이터에 대한 재훈련으로 구성됩니다. 이는 사전 훈련된 특성을 새로운 데이터에 점진적으로 적용함으로써 의미 있는 개선을 달성할 수 있습니다.\n",
        "\n",
        "먼저, Keras의 `trainable` API에 대해 자세히 살펴보겠습니다. 이 API는 대부분의 전의 학습 및 fine-tuning 워크플로우의 기초가 됩니다.\n",
        "\n",
        "그런 다음 ImageNet 데이터세트에서 사전 훈련된 모델을 사용하여 Kaggle \"cats vs dogs\" 분류 데이터세트에서 재훈련함으로써 일반적인 워크플로우를 시연합니다.\n",
        "\n",
        "이것은 [Deep Learning with Python](https://www.manning.com/books/deep-learning-with-python)과 2016 블로그 게시물 [\"building powerful image classification models using very little data\"](https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html)로부터 조정되었습니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "fbf8630c325b"
      },
      "source": [
        "## 레이어 동결: `trainable` 속성 이해\n",
        "\n",
        "레이어 및 모델에는 세 가지 가중치 속성이 있습니다.\n",
        "\n",
        "- `weights`는 레이어의 모든 가중치 변수 목록입니다.\n",
        "- `trainable_weights`는 훈련 중 손실을 최소화하기 위해 업데이트(그래디언트 디센트를 통해)되어야 하는 목록입니다.\n",
        "- `non_trainable_weights`는 훈련되지 않은 것들의 목록입니다. 일반적으로 순방향 전달 중에 모델에 의해 업데이트됩니다.\n",
        "\n",
        "**예제: `Dense` 레이어에는 2개의 학습 가능한 가중치가 있습니다(커널 및 바이어스)**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "407deab1855e"
      },
      "outputs": [],
      "source": [
        "layer = keras.layers.Dense(3)\n",
        "layer.build((None, 4))  # Create the weights\n",
        "\n",
        "print(\"weights:\", len(layer.weights))\n",
        "print(\"trainable_weights:\", len(layer.trainable_weights))\n",
        "print(\"non_trainable_weights:\", len(layer.non_trainable_weights))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "79fcb9cc960d"
      },
      "source": [
        "일반적으로 모든 가중치는 훈련이 가능합니다. 훈련할 수 없는 가중치가 있는 유일한 내장 레이어는 `BatchNormalization` 레이어입니다. 훈련할 수 없는 가중치를 사용하여 훈련 중 입력의 평균 및 분산을 추적합니다. 사용자 정의 레이어에서 훈련할 수 없는 가중치를 사용하는 방법을 배우려면 [새 레이어를 처음부터 작성하는 방법](https://keras.io/guides/making_new_layers_and_models_via_subclassing/)을 참조하세요.\n",
        "\n",
        "**예제: `BatchNormalization` 레이어에는 2개의 훈련 가능한 가중치와 2개의 훈련할 수 없는 가중치가 있습니다**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "fbc87a09bc3c"
      },
      "outputs": [],
      "source": [
        "layer = keras.layers.BatchNormalization()\n",
        "layer.build((None, 4))  # Create the weights\n",
        "\n",
        "print(\"weights:\", len(layer.weights))\n",
        "print(\"trainable_weights:\", len(layer.trainable_weights))\n",
        "print(\"non_trainable_weights:\", len(layer.non_trainable_weights))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "cddcdbf2bd5b"
      },
      "source": [
        "레이어 및 모델에는 boolean 속성 `trainable`도 있습니다. 값은 변경될 수 있습니다. `layer.trainable`을 `False`로 설정하면 모든 레이어의 가중치가 훈련 가능에서 훈련 불가능으로 이동합니다. 이를 레이어 \"동결\"이라고 합니다. 동결 레이어의 상태는 `fit()`을 사용하거나 `trainable_weights`에 의존하는 사용자 정의 루프를 사용해 훈련하여 그래디언트 업데이트를 적용할 때도 훈련하는 동안 업데이트되지 않습니다.\n",
        "\n",
        "**예제: `trainable`을 `False`로 설정**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "51bbc5d12742"
      },
      "outputs": [],
      "source": [
        "layer = keras.layers.Dense(3)\n",
        "layer.build((None, 4))  # Create the weights\n",
        "layer.trainable = False  # Freeze the layer\n",
        "\n",
        "print(\"weights:\", len(layer.weights))\n",
        "print(\"trainable_weights:\", len(layer.trainable_weights))\n",
        "print(\"non_trainable_weights:\", len(layer.non_trainable_weights))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "32904f9a58db"
      },
      "source": [
        "훈련 가능한 가중치가 훈련할 수 없게 되면 훈련 중에 그 값이 더는 업데이트되지 않습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3c26c27a8291"
      },
      "outputs": [],
      "source": [
        "# Make a model with 2 layers\n",
        "layer1 = keras.layers.Dense(3, activation=\"relu\")\n",
        "layer2 = keras.layers.Dense(3, activation=\"sigmoid\")\n",
        "model = keras.Sequential([keras.Input(shape=(3,)), layer1, layer2])\n",
        "\n",
        "# Freeze the first layer\n",
        "layer1.trainable = False\n",
        "\n",
        "# Keep a copy of the weights of layer1 for later reference\n",
        "initial_layer1_weights_values = layer1.get_weights()\n",
        "\n",
        "# Train the model\n",
        "model.compile(optimizer=\"adam\", loss=\"mse\")\n",
        "model.fit(np.random.random((2, 3)), np.random.random((2, 3)))\n",
        "\n",
        "# Check that the weights of layer1 have not changed during training\n",
        "final_layer1_weights_values = layer1.get_weights()\n",
        "np.testing.assert_allclose(\n",
        "    initial_layer1_weights_values[0], final_layer1_weights_values[0]\n",
        ")\n",
        "np.testing.assert_allclose(\n",
        "    initial_layer1_weights_values[1], final_layer1_weights_values[1]\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "412d7d659aa1"
      },
      "source": [
        "`layer.trainable` 속성을 레이어가 추론 모드 또는 트레이닝 모드에서 순방향 패스를 실행해야하는지를 제어하는 `layer.__call__()`의 인수 `훈련`과 혼동하지 마십시오. 자세한 내용은 [Keras FAQ](https://keras.io/getting_started/faq/#whats-the-difference-between-the-training-argument-in-call-and-the-trainable-attribute)를 참조하세요."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "e6ccd3c7ab1a"
      },
      "source": [
        "## `trainable` 속성의 재귀 설정\n",
        "\n",
        "모델 또는 하위 레이어가 있는 레이어에서 `trainable = False`를 설정하면 모든 하위 레이어도 훈련할 수 없게 됩니다.\n",
        "\n",
        "**예:**"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "4235d0c69821"
      },
      "outputs": [],
      "source": [
        "inner_model = keras.Sequential(\n",
        "    [\n",
        "        keras.Input(shape=(3,)),\n",
        "        keras.layers.Dense(3, activation=\"relu\"),\n",
        "        keras.layers.Dense(3, activation=\"relu\"),\n",
        "    ]\n",
        ")\n",
        "\n",
        "model = keras.Sequential(\n",
        "    [keras.Input(shape=(3,)), inner_model, keras.layers.Dense(3, activation=\"sigmoid\"),]\n",
        ")\n",
        "\n",
        "model.trainable = False  # Freeze the outer model\n",
        "\n",
        "assert inner_model.trainable == False  # All layers in `model` are now frozen\n",
        "assert inner_model.layers[0].trainable == False  # `trainable` is propagated recursively"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "61535ba76727"
      },
      "source": [
        "## 일반적인 전이 학습 워크플로\n",
        "\n",
        "이를 통해 Keras에서 일반적인 전이 학습 워크플로우를 구현할 수 있습니다.\n",
        "\n",
        "1. 기본 모델을 인스턴스화하고 사전 훈련된 가중치를 로드합니다.\n",
        "2. `trainable = False`를 설정하여 기본 모델의 모든 레이어를 동결합니다.\n",
        "3. 기본 모델에서 하나 이상의 레이어 출력 위에 새 모델을 만듭니다.\n",
        "4. 새 데이터세트에서 새 모델을 학습합니다.\n",
        "\n",
        "보다 가벼운 대안 워크플로우는 다음과 같습니다.\n",
        "\n",
        "1. 기본 모델을 인스턴스화하고 사전 훈련된 가중치를 로드합니다.\n",
        "2. 이를 통해 새로운 데이터세트를 실행하고 기본 모델의 하나의(또는 여러) 레이어의 출력을 기록합니다. 이를 **특성 추출**이라 합니다.\n",
        "3. 이 출력을 더 작은 새 모델의 입력 데이터로 사용합니다.\n",
        "\n",
        "이 두 번째 워크플로의 주요 장점은 훈련 epoch마다 한 번이 아니라 한 번의 데이터로 기본 모델을 실행한다는 것입니다. 따라서 훨씬 빠르고 저렴합니다.\n",
        "\n",
        "그러나 두 번째 워크플로의 문제점은 훈련 중에 새 모델의 입력 데이터를 동적으로 수정할 수 없다는 것입니다. 예를 들어 데이터 증강을 수행할 때 필요합니다. 전이 학습은 일반적으로 새 데이터세트에 데이터가 너무 작아서 전체 규모의 모델을 처음부터 학습할 수 없는 작업에 사용되며, 이러한 시나리오에서는 데이터 증강이 매우 중요합니다. 따라서 다음 내용에서는 첫 번째 워크플로에 중점을 둘 것입니다.\n",
        "\n",
        "Keras의 첫 번째 워크플로는 다음과 같습니다.\n",
        "\n",
        "먼저, 사전 훈련된 가중치를 사용하여 기본 모델을 인스턴스화합니다.\n",
        "\n",
        "```python\n",
        "base_model = keras.applications.Xception(\n",
        "    weights='imagenet',  # Load weights pre-trained on ImageNet.\n",
        "    input_shape=(150, 150, 3),\n",
        "    include_top=False)  # Do not include the ImageNet classifier at the top.\n",
        "```\n",
        "\n",
        "그런 다음 기본 모델을 동결합니다.\n",
        "\n",
        "```python\n",
        "base_model.trainable = False\n",
        "```\n",
        "\n",
        "맨 위에 새 모델을 만듭니다.\n",
        "\n",
        "```python\n",
        "inputs = keras.Input(shape=(150, 150, 3))\n",
        "# We make sure that the base_model is running in inference mode here,\n",
        "# by passing `training=False`. This is important for fine-tuning, as you will\n",
        "# learn in a few paragraphs.\n",
        "x = base_model(inputs, training=False)\n",
        "# Convert features of shape `base_model.output_shape[1:]` to vectors\n",
        "x = keras.layers.GlobalAveragePooling2D()(x)\n",
        "# A Dense classifier with a single unit (binary classification)\n",
        "outputs = keras.layers.Dense(1)(x)\n",
        "model = keras.Model(inputs, outputs)\n",
        "```\n",
        "\n",
        "새로운 데이터에 대한 모델을 훈련합니다.\n",
        "\n",
        "```python\n",
        "model.compile(optimizer=keras.optimizers.Adam(),\n",
        "              loss=keras.losses.BinaryCrossentropy(from_logits=True),\n",
        "              metrics=[keras.metrics.BinaryAccuracy()])\n",
        "model.fit(new_dataset, epochs=20, callbacks=..., validation_data=...)\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "736c99aea690"
      },
      "source": [
        "## Fine-tuning\n",
        "\n",
        "모델이 새로운 데이터에 수렴하면 기본 모델의 일부 또는 전부를 동결 해제하고 학습 속도가 매우 낮은 전체 모델을 전체적으로 재훈련할 수 있습니다.\n",
        "\n",
        "이 단계는 선택적으로 마지막 단계이며 점진적으로 개선할 수 있습니다. 또한 잠재적으로 빠른 과적합을 초래할 수 있습니다. 명심하십시오.\n",
        "\n",
        "동결된 레이어가 있는 모델이 수렴하도록 훈련된*후에*만 이 단계를 수행하는 것이 중요합니다. 무작위로 초기화된 훈련 가능한 레이어를 사전 훈련된 특성을 보유하는 훈련 가능한 레이어와 혼합하는 경우, 무작위로 초기화된 레이어는 훈련 중에 매우 큰 그래디언트 업데이트를 유발하여 사전 훈련된 특성을 파괴합니다.\n",
        "\n",
        "또한 이 단계에서는 일반적으로 매우 작은 데이터 집합에서 첫 번째 훈련보다 훨씬 더 큰 모델을 훈련하기 때문에, 매우 낮은 학습 속도를 사용하는 것이 중요합니다. 결과적으로 큰 가중치 업데이트를 적용하면 과도하게 빠른 과적합의 위험이 있습니다. 여기에서는 사전 훈련된 가중치만 점진적인 방식으로 다시 적용하려고 합니다.\n",
        "\n",
        "다음은 전체 기본 모델의 fine-tuning을 구현하는 방법입니다.\n",
        "\n",
        "```python\n",
        "# Unfreeze the base model\n",
        "base_model.trainable = True\n",
        "\n",
        "# It's important to recompile your model after you make any changes\n",
        "# to the `trainable` attribute of any inner layer, so that your changes\n",
        "# are take into account\n",
        "model.compile(optimizer=keras.optimizers.Adam(1e-5),  # Very low learning rate\n",
        "              loss=keras.losses.BinaryCrossentropy(from_logits=True),\n",
        "              metrics=[keras.metrics.BinaryAccuracy()])\n",
        "\n",
        "# Train end-to-end. Be careful to stop before you overfit!\n",
        "model.fit(new_dataset, epochs=10, callbacks=..., validation_data=...)\n",
        "```\n",
        "\n",
        "**`compile()` 및 `trainable`에 대한 중요한 사항**\n",
        "\n",
        "모델에서 `compile()`을 호출하는 것은 해당 모델의 동작을 \"동결\"하기 위한 것입니다. 이는 `compile`이 다시 호출될 때까지 모델이 컴파일될 때 `trainable` 속성값이 해당 모델의 수명 동안 유지되어야 함을 의미합니다. 따라서 `trainable` 값을 변경하면 모델에서 `compile()`을 다시 호출하여 변경 사항을 적용해야 합니다.\n",
        "\n",
        "**`BatchNormalization` 레이어에 대한 중요한 사항**\n",
        "\n",
        "많은 이미지 모델에는 `BatchNormalization` 레이어가 포함되어 있습니다. 그 레이어는 상상할 수 있는 모든 수에서 특별한 경우입니다. 다음은 명심해야할 몇 가지 사항입니다.\n",
        "\n",
        "- `BatchNormalization`에는 훈련 중에 업데이트되는 훈련 불가능한 2개의 가중치가 포함되어 있습니다. 입력의 평균과 분산을 추적하는 변수입니다.\n",
        "- `bn_layer.trainable = False`를 설정하면 `BatchNormalization` 레이어가 추론 모드에서 실행되며 평균 및 분산 통계가 업데이트되지 않습니다. [가중치 훈련 및 추론/훈련 모드가 직교 개념](https://keras.io/getting_started/faq/#whats-the-difference-between-the-training-argument-in-call-and-the-trainable-attribute)이므로 일반적으로 다른 레이어의 경우에는 해당되지 않습니다. 그러나 `BatchNormalization` 레이어의 경우 두 가지가 묶여 있습니다.\n",
        "- 미세 조정을 위해 `BatchNormalization` 레이어를 포함하는 모델을 동결 해제하면 기본 모델을 호출할 때 `training=False`를 전달하여 `BatchNormalization` 레이어를 추론 모드로 유지해야 합니다. 그렇지 않으면 훈련 불가능한 가중치에 적용된 업데이트로 인해 모델이 학습한 내용이 갑작스럽게 파괴됩니다.\n",
        "\n",
        "이 가이드 끝의 엔드 투 엔드 예제에서 이 패턴이 적용되는 것을 볼 수 있습니다.\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bce9ffc4e290"
      },
      "source": [
        "## 사용자 지정 훈련 루프를 사용한 전이 학습 및 fine-tuning\n",
        "\n",
        "`fit()` 대신 자체 저수준 훈련 루프를 사용하는 경우 워크플로는 본질적으로 동일하게 유지됩니다. 그래디언트 업데이트를 적용할 때 목록 `model.trainable_weights`만 고려해야 합니다.\n",
        "\n",
        "```python\n",
        "# Create base model\n",
        "base_model = keras.applications.Xception(\n",
        "    weights='imagenet',\n",
        "    input_shape=(150, 150, 3),\n",
        "    include_top=False)\n",
        "# Freeze base model\n",
        "base_model.trainable = False\n",
        "\n",
        "# Create new model on top.\n",
        "inputs = keras.Input(shape=(150, 150, 3))\n",
        "x = base_model(inputs, training=False)\n",
        "x = keras.layers.GlobalAveragePooling2D()(x)\n",
        "outputs = keras.layers.Dense(1)(x)\n",
        "model = keras.Model(inputs, outputs)\n",
        "\n",
        "loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)\n",
        "optimizer = keras.optimizers.Adam()\n",
        "\n",
        "# Iterate over the batches of a dataset.\n",
        "for inputs, targets in new_dataset:\n",
        "    # Open a GradientTape.\n",
        "    with tf.GradientTape() as tape:\n",
        "        # Forward pass.\n",
        "        predictions = model(inputs)\n",
        "        # Compute the loss value for this batch.\n",
        "        loss_value = loss_fn(targets, predictions)\n",
        "\n",
        "    # Get gradients of loss wrt the *trainable* weights.\n",
        "    gradients = tape.gradient(loss_value, model.trainable_weights)\n",
        "    # Update the weights of the model.\n",
        "    optimizer.apply_gradients(zip(gradients, model.trainable_weights))\n",
        "```"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "4e63ba34ce1c"
      },
      "source": [
        "fine-tuning의 경우도 마찬가지입니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "852447087ba9"
      },
      "source": [
        "## 엔드 투 엔드 예제: 고양이 vs 개 데이터세트에서 이미지 분류 모델 미세 조정\n",
        "\n",
        "이러한 개념을 강화하기 위해 구체적인 엔드 투 엔드 전이 학습 및 fine-tuning 예제를 안내합니다. ImageNet에서 사전 훈련된 Xception 모델을 로드하고 Kaggle \"cats vs. dogs\" 분류 데이터세트에서 사용합니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "ba75835e0de6"
      },
      "source": [
        "### 데이터 얻기\n",
        "\n",
        "먼저 TFDS를 사용해 고양이 vs 개 데이터세트를 가져옵니다. 자체 데이터세트가 있다면 유틸리티 `tf.keras.preprocessing.image_dataset_from_directory`를 사용하여 클래스 별 폴더에 파일링 된 디스크의 이미지 세트에서 유사한 레이블이 지정된 데이터세트 오브젝트를 생성할 수 있습니다.\n",
        "\n",
        "전이 학습은 매우 작은 데이터로 작업할 때 가장 유용합니다. 데이터세트를 작게 유지하기 위해 원래 훈련 데이터(25,000개 이미지)의 40%를 훈련에, 10%를 유효성 검사에, 10%를 테스트에 사용합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "1a99f56934f7"
      },
      "outputs": [],
      "source": [
        "import tensorflow_datasets as tfds\n",
        "\n",
        "tfds.disable_progress_bar()\n",
        "\n",
        "train_ds, validation_ds, test_ds = tfds.load(\n",
        "    \"cats_vs_dogs\",\n",
        "    # Reserve 10% for validation and 10% for test\n",
        "    split=[\"train[:40%]\", \"train[40%:50%]\", \"train[50%:60%]\"],\n",
        "    as_supervised=True,  # Include labels\n",
        ")\n",
        "\n",
        "print(\"Number of training samples: %d\" % tf.data.experimental.cardinality(train_ds))\n",
        "print(\n",
        "    \"Number of validation samples: %d\" % tf.data.experimental.cardinality(validation_ds)\n",
        ")\n",
        "print(\"Number of test samples: %d\" % tf.data.experimental.cardinality(test_ds))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "9db548603642"
      },
      "source": [
        "이것은 훈련 데이터세트에서 처음 9개의 이미지입니다. 보시다시피 이미지는 모두 크기가 다릅니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "00c8cbd1de88"
      },
      "outputs": [],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "plt.figure(figsize=(10, 10))\n",
        "for i, (image, label) in enumerate(train_ds.take(9)):\n",
        "    ax = plt.subplot(3, 3, i + 1)\n",
        "    plt.imshow(image)\n",
        "    plt.title(int(label))\n",
        "    plt.axis(\"off\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "168c4a10c072"
      },
      "source": [
        "레이블 1이 \"개\"이고 레이블 0이 \"고양이\"임을 알 수 있습니다."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "f749203cd740"
      },
      "source": [
        "### 데이터 표준화하기\n",
        "\n",
        "우리의 원시 이미지는 다양한 크기를 가지고 있습니다. 또한 각 픽셀은 0에서 255 사이의 3개의 정수값(RGB 레벨값)으로 구성됩니다. 이것은 신경 네트워크에 먹이를 주는 데 적합하지 않습니다. 우리는 2가지를 해야 합니다:\n",
        "\n",
        "- 고정된 이미지 크기로 표준화합니다. 150x150을 선택합니다.\n",
        "- 정상 픽셀 값은 -1 과 1 사이입니다. 우리는 모델 자체의 일부로 `Normalization` 레이어를 사용합니다.\n",
        "\n",
        "일반적으로 이미 사전 처리된 데이터를 사용하는 모델과 달리 원시 데이터를 입력으로 사용하는 모델을 개발하는 것이 좋습니다. 모델에 사전 처리 된 데이터가 필요한 경우 모델을 내보내 다른 위치(웹 브라우저, 모바일 앱)에서 사용할 때마다 동일한 사전 처리 파이프 라인을 다시 구현해야하기 때문입니다. 이것은 매우 까다로워집니다. 따라서 모델에 도달하기 전에 가능한 최소한의 전처리를 수행해야합니다.\n",
        "\n",
        "여기에서는 데이터 파이프라인에서 이미지 크기 조정을 수행하고(심층 신경망은 인접한 데이터 배치만 처리할 수 있기 때문에) 입력값 스케일링을 모델의 일부로 생성합니다.\n",
        "\n",
        "이미지 크기를 150x150으로 조정해 보겠습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "b3678f38e087"
      },
      "outputs": [],
      "source": [
        "size = (150, 150)\n",
        "\n",
        "train_ds = train_ds.map(lambda x, y: (tf.image.resize(x, size), y))\n",
        "validation_ds = validation_ds.map(lambda x, y: (tf.image.resize(x, size), y))\n",
        "test_ds = test_ds.map(lambda x, y: (tf.image.resize(x, size), y))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "708bf9792a35"
      },
      "source": [
        "또한 데이터를 일괄 처리하고 캐싱 및 프리페치를 사용하여 로딩 속도를 최적화합니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "53ef9e6092e3"
      },
      "outputs": [],
      "source": [
        "batch_size = 32\n",
        "\n",
        "train_ds = train_ds.cache().batch(batch_size).prefetch(buffer_size=10)\n",
        "validation_ds = validation_ds.cache().batch(batch_size).prefetch(buffer_size=10)\n",
        "test_ds = test_ds.cache().batch(batch_size).prefetch(buffer_size=10)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "b60f852c462f"
      },
      "source": [
        "### 무작위 데이터 증강 사용하기\n",
        "\n",
        "큰 이미지 데이터세트가 없는 경우 임의의 수평 뒤집기 또는 작은 임의의 회전과 같이 훈련 이미지에 무작위이지만 사실적인 변형을 적용하여 샘플 다양성을 인위적으로 도입하는 것이 좋습니다. 이것은 과적합을 늦추면서 모델을 훈련 데이터의 다른 측면에 노출하는 데 도움이 됩니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "40b1e355b9c0"
      },
      "outputs": [],
      "source": [
        "from tensorflow import keras\n",
        "from tensorflow.keras import layers\n",
        "\n",
        "data_augmentation = keras.Sequential(\n",
        "    [\n",
        "        layers.experimental.preprocessing.RandomFlip(\"horizontal\"),\n",
        "        layers.experimental.preprocessing.RandomRotation(0.1),\n",
        "    ]\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "6fa8ddeda36e"
      },
      "source": [
        "다양한 무작위 변형 후 첫 번째 배치의 첫 번째 이미지가 어떻게 보이는지 시각화해 보겠습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9077f9fd022e"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "\n",
        "for images, labels in train_ds.take(1):\n",
        "    plt.figure(figsize=(10, 10))\n",
        "    first_image = images[0]\n",
        "    for i in range(9):\n",
        "        ax = plt.subplot(3, 3, i + 1)\n",
        "        augmented_image = data_augmentation(\n",
        "            tf.expand_dims(first_image, 0), training=True\n",
        "        )\n",
        "        plt.imshow(augmented_image[0].numpy().astype(\"int32\"))\n",
        "        plt.title(int(labels[0]))\n",
        "        plt.axis(\"off\")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "bc999e4672c3"
      },
      "source": [
        "## 모델 구축\n",
        "\n",
        "이제 앞에서 설명한 청사진을 따르는 모델을 만들어 보겠습니다.\n",
        "\n",
        "주의할 사항:\n",
        "\n",
        "- 입력 값(처음에는 `[0, 255]` 범위)을 `[-1, 1]` 범위로 조정하기 위해 `Normalization` 레이어를 추가합니다.\n",
        "- 정규화를 위해 분류 레이어 앞에 `Dropout` 레이어를 추가합니다.\n",
        "- 기본 모델을 호출 할 때 `training=False`를 전달하여 추론 모드에서 실행되므로 fine-tuning을 위해 기본 모델을 동결 해제한 후에도 batchnorm 통계가 업데이트되지 않습니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "07a2f9e9d817"
      },
      "outputs": [],
      "source": [
        "base_model = keras.applications.Xception(\n",
        "    weights=\"imagenet\",  # Load weights pre-trained on ImageNet.\n",
        "    input_shape=(150, 150, 3),\n",
        "    include_top=False,\n",
        ")  # Do not include the ImageNet classifier at the top.\n",
        "\n",
        "# Freeze the base_model\n",
        "base_model.trainable = False\n",
        "\n",
        "# Create new model on top\n",
        "inputs = keras.Input(shape=(150, 150, 3))\n",
        "x = data_augmentation(inputs)  # Apply random data augmentation\n",
        "\n",
        "# Pre-trained Xception weights requires that input be normalized\n",
        "# from (0, 255) to a range (-1., +1.), the normalization layer\n",
        "# does the following, outputs = (inputs - mean) / sqrt(var)\n",
        "norm_layer = keras.layers.experimental.preprocessing.Normalization()\n",
        "mean = np.array([127.5] * 3)\n",
        "var = mean ** 2\n",
        "# Scale inputs to [-1, +1]\n",
        "x = norm_layer(x)\n",
        "norm_layer.set_weights([mean, var])\n",
        "\n",
        "# The base model contains batchnorm layers. We want to keep them in inference mode\n",
        "# when we unfreeze the base model for fine-tuning, so we make sure that the\n",
        "# base_model is running in inference mode here.\n",
        "x = base_model(x, training=False)\n",
        "x = keras.layers.GlobalAveragePooling2D()(x)\n",
        "x = keras.layers.Dropout(0.2)(x)  # Regularize with dropout\n",
        "outputs = keras.layers.Dense(1)(x)\n",
        "model = keras.Model(inputs, outputs)\n",
        "\n",
        "model.summary()"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "2e8237de81e8"
      },
      "source": [
        "## 최상위 레이어 훈련"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "9137b8daedad"
      },
      "outputs": [],
      "source": [
        "model.compile(\n",
        "    optimizer=keras.optimizers.Adam(),\n",
        "    loss=keras.losses.BinaryCrossentropy(from_logits=True),\n",
        "    metrics=[keras.metrics.BinaryAccuracy()],\n",
        ")\n",
        "\n",
        "epochs = 20\n",
        "model.fit(train_ds, epochs=epochs, validation_data=validation_ds)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "aa51d4562fa7"
      },
      "source": [
        "## 전체 모델의 fine-tuning을 수행합니다\n",
        "\n",
        "마지막으로 기본 모델을 동결 해제하고 낮은 학습 속도로 전체 모델을 전체적으로 학습합니다.\n",
        "\n",
        "중요한 것은 기본 모델이 학습 가능하지만 모델을 빌드 모델을 호출 할 때 `training=False`를 전달했기 때문에 여전히 추론 모드에서 실행되고 있다는 것입니다. 이는 내부의 배치 정규화 레이어가 배치 통계를 업데이트하지 않음을 의미합니다. 만약 그들이 그렇게한다면, 그들은 지금까지 모델이 배운 표현에 혼란을 줄 것입니다."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {
        "id": "3cc299505b72"
      },
      "outputs": [],
      "source": [
        "# Unfreeze the base_model. Note that it keeps running in inference mode\n",
        "# since we passed `training=False` when calling it. This means that\n",
        "# the batchnorm layers will not update their batch statistics.\n",
        "# This prevents the batchnorm layers from undoing all the training\n",
        "# we've done so far.\n",
        "base_model.trainable = True\n",
        "model.summary()\n",
        "\n",
        "model.compile(\n",
        "    optimizer=keras.optimizers.Adam(1e-5),  # Low learning rate\n",
        "    loss=keras.losses.BinaryCrossentropy(from_logits=True),\n",
        "    metrics=[keras.metrics.BinaryAccuracy()],\n",
        ")\n",
        "\n",
        "epochs = 10\n",
        "model.fit(train_ds, epochs=epochs, validation_data=validation_ds)"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "afa73d989302"
      },
      "source": [
        "10 epoch 후에, fine-tuning이 크게 개선됩니다."
      ]
    }
  ],
  "metadata": {
    "colab": {
      "collapsed_sections": [],
      "name": "transfer_learning.ipynb",
      "toc_visible": true
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
